<template>
  <el-tree
    draggable
    node-key="id"
    ref="fileTreeRef"
    highlight-current
    empty-text="No File"
    :data="FileTreeData"
    @node-drop="nodeDropHandle"
    @node-click="nodeClickHandle"
    :allow-drop="checkNodeAllowDrop"
    @node-contextmenu="nodeContextmenuHandle"
  >
    <template #default="{ data }">
      <!-- 处理新建 -->
      <el-input
        size="small"
        v-if="data.isNew"
        v-model="newValue"
        ref="newInputRef"
        @blur="newInputBlur"
        placeholder="Please input"
        @keydown.enter="newInputRef?.blur()"
      />

      <!-- 处理重命名 -->
      <el-input
        size="small"
        ref="renameInputRef"
        v-model="renameValue"
        v-else-if="data.rename"
        @blur="renameInputBlur"
        @keydown.enter="renameInputRef?.blur()"
      />

      <!-- 正常展示文件名称/文件夹名称， -->
      <div class="file-tree-item" v-else>
        <img
          v-if="data.type !== 'Folder'"
          :src="Utils.getFileIcon(data.label)"
        />
        <span> {{ data.label }}</span>
      </div>
    </template>
  </el-tree>

  <!-- 导入文件夹 -->
  <FolderUpload ref="folderUploadRef" @upload="uploadHandle" />
</template>

<script setup lang="ts">
import { Utils } from "../../../utils";
import { reactive, ref, watch } from "vue";
import FolderUpload from "./FolderUpload.vue";
import { Popover } from "../../../class/Popover";
import { ElMessage, type ElTree } from "element-plus";
import { nextTick, onBeforeUnmount, onMounted } from "vue";
import { IFileTreeItem } from "../../../interface/FileTree";
import WebContainerAPI from "../../../class/WebContainerAPI";
import { useEditorStore } from "../../../pinia/useEditorStore";
import { useFileTreeStore } from "../../../pinia/useFileTreeStore";
import { FileSystemTreeMock } from "../../../mock/FileSystemTreeMock";
import { TreeNodeData } from "element-plus/es/components/tree/src/tree.type.mjs";

/**
 * 定义右键菜单
 */
const menuList = [
  { callback: openinterminalHandle, title: "在集成终端打开", shortcut: "" },
  { callback: opentimelineHandle, title: "打开时间线", shortcut: "Ctrl + T" },
  { callback: copypathHandle, title: "复制相对路径", shortcut: "" },
  { callback: renameHandle, title: "重命名", shortcut: "F2" },
  { callback: deleteHandle, title: "删除", shortcut: "Delete" },
];

// 解析 pinia store
const fileTreeStore = useFileTreeStore();
const editorStore = useEditorStore();

// 文件树 El-Tree data 应该从 pinia 来
const FileTreeData = reactive(<IFileTreeItem[]>[]);

/** 监听pinia数据，更新当前 el-tree */
watch(
  () => fileTreeStore.fileTreeData,
  (data) => {
    FileTreeData.length = 0;
    data.forEach((i) => FileTreeData.push(i));
  },
  { deep: true, immediate: true }
);

// 定义 el-tree dom 节点
const fileTreeRef = ref<InstanceType<typeof ElTree> | null>(null);

// 定义 el-input dom 节点
const newInputRef = ref<HTMLInputElement | null>(null);

// 定义 rename input dom 节点
const renameInputRef = ref<HTMLInputElement | null>(null);

// 定义 newItem renameItem v-model 绑定
const newValue = ref("");
const renameValue = ref("");

// 当前高亮（激活）的节点ID
const currentNodeID = ref("");

// 节点拖拽时，需要在拖拽开始时，记录初始路径
const draggingNodePath = ref("");

// 定义导入文件组件 ref
const folderUploadRef = ref(<InstanceType<typeof FolderUpload> | null>null);

/**
 * 节点单击事件 - 设置当前节点ID，并打开文件 使用 pinia 进行数据传输
 */
function nodeClickHandle(data: IFileTreeItem) {
  setCurrentNodeID(data.id);
  editorStore.setCurrentFile(data.type === "Folder" ? null : data);
}

/**
 * 节点右键菜单点击事件 - 设置当前节点ID El-Popover 会自动打开自定义的右键菜单
 */
function nodeContextmenuHandle(e: PointerEvent, data: IFileTreeItem) {
  e.stopPropagation();
  setCurrentNodeID(data.id); // 高亮当前节点
  const { pageX, pageY } = e;
  const popover = new Popover();
  popover.show({ menuList, x: pageX, y: pageY });
}

/**
 * contextmenuItemClick 打开时间线 - 实现函数
 */
function openinterminalHandle() {}

/**
 *  contextmenuItemClick 打开时间线- 实现函数
 */
function opentimelineHandle() {}

/**
 * contextmenuItemClick 复制路径 - 实现函数
 */
function copypathHandle() {}

/**
 * contextmenuItemClick 重命名 - 实现函数(为了实现复用)
 */
async function renameHandle() {
  //  1. 获取当前激活的节点
  const currentNode = fileTreeRef.value!.getNode(currentNodeID.value);
  // 查找节点，找到 id===nodeID 修改 rename 属性为 true
  currentNode.data.rename = true;
  const label = currentNode.data.label; // 获取旧名称
  await nextTick();
  renameValue.value = label;
  renameInputRef.value?.focus();
}

/**
 * contextmenuItemClick 删除文件 - 实现函数
 */
async function deleteHandle() {}

/**
 * 判断节点是否可被拖拽
 *  1. 两个不同文件夹下的文件拖拽 after before 是被允许的
 *  2. 同一个文件夹内的文件拖拽顺序是无法调整的，因为该顺序按照文件名默认排序
 *  3. 禁止拖拽到文件内！
 *
 * @param draggingNode 拖拽的节点
 * @param dropNode 拖拽到的节点
 * @param type 拖拽的类型，inner 表示拖拽到目标节点内部，before after 表示拖拽到目标节点的前面或后面
 * @returns boolean 是否允许拖拽
 */
function checkNodeAllowDrop(
  draggingNode: TreeNodeData,
  dropNode: TreeNodeData,
  type: string
) {
  const dataMap = getDataMap();
  // 获取正在拖拽的文件ID
  const draggingNodeID = draggingNode.data.id;
  // 这个值后面会用到，用于记录初始的文件路径，执行 WebContainer 中的 rename
  draggingNodePath.value = Utils.getFilePath(dataMap, draggingNodeID, true);

  // 场景一：如果拖拽进入同一个文件夹（同一个父级ID）
  if (draggingNode.parent!.data.id === dropNode.parent!.data.id) {
    // 在 vocode 中，同一个文件夹内的文件拖拽顺序是无法调整的，因为该顺序按照 文件名默认排序
    if (type !== "inner") return false;
    // 如果目标节点是文件夹，则允许拖拽
    if (dropNode.data.type === "Folder") return true;
  }
  // 场景二：如果拖拽进入不同文件夹，则需要判断目标是否为文件，因为文件不允许被拖拽进入
  return dropNode.data.type !== "File";
}

/**
 * 节点拖拽完成后，需要执行 Web Container 的 rename 方法
 * @param cnode 当前被拖拽的节点
 */
async function nodeDropHandle(cnode: TreeNodeData) {
  const dataMap = getDataMap();
  const path = Utils.getFilePath(dataMap, cnode.data.id, true);
  await WebContainerAPI.rename(draggingNodePath.value, path);
  // 清空 draggingNodePath
  draggingNodePath.value = "";
  setCurrentNodeID(path); // 注意这里哈，这个需要更新为拖拽后的路径
  updateFileTree();
}

/**
 * TopMenu.vue 顶部菜单点击事件
 */
async function menuClick(icon: string) {
  switch (icon) {
    case "icon-newfolder":
    case "icon-xinjianwenjian":
      // 向当前激活节点的同节点末尾，插入一条 isNew 的数据，页面即可显示输入框
      const type = icon === "icon-newfolder" ? "Folder" : "File";
      const data = <IFileTreeItem>{ id: "", isNew: true, type }; // 此处ID暂时为空，后续等 input blur 会更新
      // 将创建的 isNew 数据项插入 el-tree 中，并获取输入框交点
      await appendNodeData(data);
      await nextTick();
      newInputRef.value?.focus();
      break;

    // 刷新
    case "icon-shuaxin":
      // 重新定位到当前编辑区打开的文件位置
      // 刷新最新的 pinia 数据
      await fileTreeStore.render();
      break;

    // 折叠
    case "icon-ico_niantiejiankongrenwu":
      // 获取节点映射
      const nodeMap = fileTreeRef.value!.store.nodesMap;
      Object.values(nodeMap).forEach((v) => v.collapse());
      setCurrentNodeID(undefined); // 取消当前选中状态
      // 刷新最新的 pinia 数据
      await fileTreeStore.render();
      break;
  }
}

/**
 * TopMenu.vue 下拉菜单事件响应
 */
function handleCommand(command: string) {
  if (command === "import-file") return importFileHandle();
  if (command === "import-folder") return folderUploadRef.value?.openDialog();
  if (command === "init-default") return initDefaultProject();
}

/**
 * 初始化默认项目
 */
async function initDefaultProject() {
  await WebContainerAPI.mountFileTree(FileSystemTreeMock);
  await fileTreeStore.render();
}

/**
 * 导入文件
 */
function importFileHandle() {
  // 导入文件 - 创建 input
  const dataMap = getDataMap();
  const input = document.createElement("input");
  input.type = "file";
  input.multiple = true; // 多选
  input.addEventListener("change", async () => {
    const files = input.files;
    if (!files) return;
    for (const file of files) {
      const label = file.name;

      // 根据导入的文件，创建 data 数据
      const imageType = file.type.startsWith("image");
      const type = imageType ? "Image" : "File";
      const data = <IFileTreeItem>{ id: "", label, type };
      const path = Utils.getFilePath(dataMap, currentNodeID.value);
      // 更正ID
      data.id = `${path}/${data.label}`;

      /**
       * 读取 file 内容
       *  File 类型可以直接读取为 string
       *  图片类 应该读取成 Unit8Array 进行存储
       */
      const reader = new FileReader();
      if (imageType) reader.readAsDataURL(file);
      else reader.readAsText(file);

      reader.onload = async () => {
        // 读取文件内容
        const result = reader.result as string;
        // 执行真正的 Web Container 写入文件内容操作
        await WebContainerAPI.writeFile(`${path}/${label}`, result);
        // 2. 映射 el-tree 数据源
        await appendNodeData(data);

        // 设置最新节点激活
        setCurrentNodeID(data.id);

        await updateFileTree();
      };
    }
  });
  input.click();
}

/**
 * 导入文件夹成功回调
 */
async function uploadHandle(payload: {
  uploadFolderList: string[];
  uploadFileList: IFileTreeItem[];
}) {
  const { uploadFolderList, uploadFileList } = payload;
  // 获取当前的根路径
  const dataMap = getDataMap();
  const rootPath = Utils.getFilePath(dataMap, currentNodeID.value);

  // 创建目录
  for (const path of uploadFolderList) {
    await WebContainerAPI.createFolder(rootPath + path);
  }

  // 接着创建文件，不然先创建文件会报没有当前文件夹的错误
  for (const file of uploadFileList) {
    await WebContainerAPI.writeFile(rootPath + file.id, file.content!);
  }
  await updateFileTree();
}

/**
 * 向 El-Tree 插入节点数据
 *  这个是重要的辅助函数，会识别当前激活节点的类型，是文件的话，添加到最后，是文件夹，添加到内部
 */
async function appendNodeData(data: IFileTreeItem) {
  const dataMap = getDataMap();
  const cid = currentNodeID.value;
  // 当前是否有节点被选中
  if (cid) {
    // 如果有节点被选中，则看当前节点是文件，还是文件夹，是文件-在父级添加，是文件夹-直接在当前文件夹添加
    const currentNode = fileTreeRef.value?.getNode(cid);
    const type = currentNode?.data?.type;
    if (type === "File") {
      fileTreeRef.value?.insertAfter(data, cid);
    } else {
      // 如果当前激活的是文件夹
      fileTreeRef.value?.append(data, cid);
      await nextTick();
      expandNodeByID(cid);
      await nextTick();
    }
  }

  // 不然直接添加到当前树形结构的最后面一项 - data是props 传递下来的，因此不需要再做处理，直接更新 pinia 即可
  else dataMap.push(data);
}

/**
 * 新建文件/文件夹输入框失焦事件
 *  1. 删除相关 input 元素
 *  2. 确认新建文件/文件夹
 *  3. 刷新文件树
 *  4. 这里注意回调函数的用法哈
 *  5. 新建文件夹时，还需要调整顺序！
 */
async function newInputBlur() {
  const dataMap = getDataMap();

  // 此时，需要拿到唯一 isNew 的数据项
  const isNewItem = Utils.findItem(dataMap, (i) => Boolean(i.isNew));
  if (!isNewItem) return;

  // 不管有没有输入名称，只要isNew存在，则一定需要删除 isnew 属性
  isNewItem.isNew = false;

  // 如果没有输入名称，则应该删除该记录
  if (!newValue.value) {
    Utils.deleteItem(dataMap, (i) => i.id === isNewItem.id);
    return;
  }

  // 直接返回不带文件名的路径 - src测试目录/文件夹/ or test/
  const path = Utils.getFilePath(dataMap, isNewItem.id);

  // 更正ID
  isNewItem.id = `${path}/${newValue.value}`;

  // 如果存在的情况下，直接修改 isNew 的 label
  isNewItem.label = newValue.value;

  const { id, type } = isNewItem;
  const label = newValue.value;

  // 重名检测: 同一层级下，不允许出现同名
  const isExist = await checkIsExist(label, path || "");

  if (isExist) {
    Utils.deleteItem(dataMap, (i) => i.id === id);
    ElMessage.error("名称重复");
    newValue.value = "";
    return;
  }

  /** 真实的执行 webcontainer 的创建文件/文件夹操作 */
  if (type === "File") {
    await WebContainerAPI.createFile(`${path}/${label}`);
    // 创建的是文件，直接打开
    editorStore.setCurrentFile(isNewItem);
  }
  if (type === "Folder") await WebContainerAPI.createFolder(`${path}/${label}`);

  // 等待刷新
  await nextTick();

  // 设置最新节点激活
  setCurrentNodeID(id);

  await updateFileTree();

  // 清空 input 值
  newValue.value = "";
}

/**
 * 重命名输入框失焦事件
 */
async function renameInputBlur() {
  const dataMap = getDataMap();

  // 此时，需要拿到唯一 rename 的数据项
  const isRenameItem = Utils.findItem(dataMap, (i) => Boolean(i.rename));
  if (!isRenameItem) return;

  // 修改属性
  isRenameItem.rename = false;
  if (!renameValue.value) return;
  if (renameValue.value === isRenameItem.label) return;

  const path = Utils.getFilePath(dataMap, isRenameItem.id);
  const oldpath = `${path}/${isRenameItem.label}`;
  const newpath = `${path}/${renameValue.value}`;

  // 执行真正的修改名称操作 Web container API
  await WebContainerAPI.rename(oldpath, newpath);

  // 不然修改label
  isRenameItem.label = renameValue.value;

  renameValue.value = "";

  setCurrentNodeID(newpath);
  await updateFileTree();
}

/**
 * 快捷键监听
 */
function keydownHandle(e: KeyboardEvent) {
  // 监听 F2
  if (e.key === "F2") renameHandle();
  // Delete
  if (e.key === "Delete") deleteHandle();
}

/**
 * 工具函数 - 判断当前 label 是否重名
 */
async function checkIsExist(label: string, path: string) {
  const pathList = (await WebContainerAPI.readDir(path)) || [];
  return pathList.find((item) => item.name === label);
}

/**
 * 通过 id 展开节点
 */
function expandNodeByID(id: string) {
  const currentNode = fileTreeRef.value?.getNode(id);
  currentNode?.expand();
}

/**
 * 工具函数 - 获取当前 El-Tree 的数据源
 */
function getDataMap() {
  return <IFileTreeItem[]>fileTreeRef.value!.store.data;
}

/**
 * 辅助函数 - updateFileTree
 */
async function updateFileTree() {
  await fileTreeStore.render();
  await nextTick();
  setCurrentNodeID(currentNodeID.value);
}

/**
 * 设置节点高亮
 *  执行 fileTreeRef.value?.setCurrentKey 方法
 *  同步更新 currentNodeID.value
 */
function setCurrentNodeID(id: string | undefined) {
  fileTreeRef.value?.setCurrentKey(id); // 传入 undefined 会取消节点选中
  currentNodeID.value = id || ""; // 还是需要手动维护该字段
}

onMounted(() => {
  window.addEventListener("keydown", keydownHandle);
});

onBeforeUnmount(() => {
  window.removeEventListener("keydown", keydownHandle);
});

/**
 * 暴露给父组件的方法
 */
defineExpose({
  menuClick,
  cancelNodeActive: () => setCurrentNodeID(undefined),
  handleCommand,
});
</script>

<style lang="css" scoped>
.el-tree {
  user-select: none;
  background-color: transparent;
}

.file-tree-item {
  flex: auto;
  display: flex;
  align-items: center;
  height: 100%;
  /* 重要属性，只有父元素隐藏，子元素才能超过显示省略号哈 */
  overflow: hidden;
}
.file-tree-item img {
  vertical-align: middle;
  height: 100%;
}
.file-tree-item span {
  width: 100%;
  height: 100%;
  line-height: 26px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.el-tree-node__content {
  border-radius: 4px;
}
:deep().is-leaf {
  display: none;
}
:deep().el-tree-node__content:hover {
  background-color: rgba(236, 245, 255, 0.6);
}
:deep().el-tree--highlight-current
  .el-tree-node.is-current
  > .el-tree-node__content {
  background-color: #ecf5ff;
}
</style>
